'''
The module contains all scheduler related tasks.

@author: Oraby
'''

import heapq
import time



class Event:
    '''
    The class as the unit enqueued in the scheduler. 
    
    @type callableFn: C{callable}
    @param callableFn: The function to be called when the event
        execution occurs.
        
    @type args: C{list}
    @type keyargs: C{dict}
    '''
    def __init__(self,callableFn,*args,**keyargs):
        self._callableFn = callableFn
        self._args  = args
        self._keyargs = keyargs
        
    def run(self):
        '''
        This method is called by the simulation manager when the
        time of the event occurs.
        '''
        self._callableFn(*self._args,**self._keyargs)

    def __str__(self):
        return "Event fn: " + str(self._callableFn) + "  with args: " + \
                str(self._args) + " and kargs: " + str(self._keyargs)

class Scheduler():
    '''
    A priority (heap) scheduler. Users should use L{Scheduler
    Singleton} unless they need a stand-alone scheduler.
    
    The scheduler is divided into time slots. Each time slot 
    contains a list that contains one or more events.
    
    Scheduler doesn't permit the existence of the same event
    twice in the same time slot. However scheduler user shouldn't
    depend on that as this may change later.
    
    @type timeDriven: C{bool}
    @param timeDriven: If set to True (default), the time of an 
    enqueued event will be a timeout in relation to the current
    real time, if False then it will be in relation the time of 
    the least event, at value of first enqueue will be in 
    relation to real time.
    '''
    
    def __init__(self,timeDriven = True):
        self._events = {}
        self._timeline = []
        self.timeDriven = timeDriven
        if timeDriven:
            self._latestTime = time.time()
        else:
            self._latestTime = 0#Start simulation from zero
        
    def enqueue(self, eventTime, targetEvent, *args, **keyargs):
        '''
        Enqueue an event in the scheduler.
        
        @type eventTime: C{float}
        @param eventTime: The time of execution of the event.
        
        @type targetEvent: L{callable}
        @param targetEvent: The callable method or funciton to be enqueued.
        
        @type args: C{list} 
        @param args: The args list to pass to the callable
        
        @type keyargs: C{dict}
        @param keyargs: The dictionary arg list to pass to the callable.
        
        
        @rtype: L{Event}
        @return: The event that was created, it can be used to remove
            the event later.
        '''
        if self.timeDriven:
            eventTime += time.time()
            _time = time.time()
        else:
            eventTime += self._latestTime
            _time = self._latestTime
            
        if _time > eventTime: #fnTime negative???
            raise "Cannot enqueue event after it's time has passed"
        
        event = Event(targetEvent,*args,**keyargs)
        
        if eventTime not in self._events:
            self._events[eventTime] = []
            heapq.heappush(self._timeline,eventTime)
            #Duplicates of time may exist in self._timeline
            
        self._events[eventTime].append(event)

        return event
            
    def nearestEventTime(self):
        '''
        Return the time of the nearest event or C{None} if there
        are no events left.
        
        @rtype: C{float} or C{None}
        @requires: The time of the event or C{None} if there are
            no events left.
        '''
        while self._timeline:
            if self._timeline[0] in self._events: # and \
            # self._events[timeline[0]]:
                return self._timeline[0]
            else:
                heapq.heappop(self._timeline) 
        
        return None
        
        
    def popNearestEvents(self):
        '''
        Returns and delete the nearest event time slot,
        the time slots contains a list of events.
        
        @rtype: C{list} L{Event}
        @return: List of the events occurring at that time slot.
        '''
        #If I'm time driven, I should not return events unless I make
        # sure that their time has come, otherwise I should return 
        # empty list. 
        if self.timeDriven:
            if self.nearestEventTime() > time.time():
                return []
        while self._timeline:
            eventTime = heapq.heappop(self._timeline)
            try:
                #For why we are doing it in try-catch see
                # the comments in scheduler.remove()
                eventsList = self._events[eventTime]
            except KeyError:
                eventsList = []
            else:
                del self._events[eventTime]
            if eventsList:
                break
        else:
            return  []#I should document that I might return empty list if I'm
                      # empty
        self._latestTime = eventTime
        return eventsList
        
    def isEmpty(self):
        '''
        Returns whether the scheduler has more events or not.
        '''
        return not self._timeline
    
    def removeEvent(self,eventTime,event):
        '''
        Remove an event from a time slot.
        
        @type eventTime: C{float}
        @param eventTime: The time of the occurance of the
            event.
            
        @type event: L{Event}
        @param event: The event to be removed.
        '''
        #try:
        self._events[eventTime].remove(event)
        if not self._events[eventTime]:
            #Reduce the dictionary size.
            del self._events[eventTime]
            #I cannot remove the value from time line as well as its a heap 
            # queue list, messing with heap queue manually would miss the 
            # heap structure and calling heaipfy() every time is processor
            # wasting, what I'll do is that I'll leave the value in timeline
            # and catch the keyError in popNearestEvent.
        #except Exception,e:
        #    print "Unable to remove event: " + str(e)
            
class SchedulerSingleton():
    '''
    A singleton for L{Scheduler}, Simulation users should use
    this to guarantee the "singularity".
    The scheduler creates a timeDriven singleton.
    '''

    singleton = None

    def __init__(self, timeDriven = True):
        '''
        If someone called class methods then __init__ 
        would be called. The method calls L{_checkSingleton}
        '''
        self._checkSingleton()

    
    def enqueue(self, time,callableFn, *args, **keyargs):
        self._checkSingleton()   
        return SchedulerSingleton.singleton.enqueue(time, callableFn, 
                                                    *args, **keyargs)
    enqueue = classmethod(enqueue)
    
    def nearestEventTime(self):
        SchedulerSingleton._checkSingleton()
        return SchedulerSingleton.singleton.nearestEventTime()
    nearestEventTime = classmethod(nearestEventTime)
    
    def popNearestEvents(self):
        return SchedulerSingleton.singleton.popNearestEvents()
    popNearestEvents = classmethod(popNearestEvents)
    
    
    def isEmpty(self):
        SchedulerSingleton._checkSingleton()
        return SchedulerSingleton.singleton.isEmpty()
    isEmpty = classmethod(isEmpty)
    
    def removeEvent(self,eventTime,event):
        SchedulerSingleton._checkSingleton()
        return SchedulerSingleton.singleton.removeEvent(eventTime,event)
    removeEvent = classmethod(removeEvent)
    
    def _checkSingleton(self):
        '''
        Creates an instance of L{Scheduler} if non exists yet.
        '''
        if SchedulerSingleton.singleton == None:
            SchedulerSingleton.singleton = Scheduler()
    _checkSingleton = classmethod(_checkSingleton)

#The SchuedulerSingleton class proved to be little slow when profiled, 
# creating a variable of the class has proved to be faster.
SchedulerSingleton = Scheduler()